//
//  Tabster.m
//  Tabster
//
//  Created by Rob Keniger <rob@menumachine.com> on 12/11/08.
//	
//  Copyright (c) 2008 Big Bang Software Pty Ltd
//
//	http://menumachine.com
//
//	Permission is hereby granted, free of charge, to any person
//	obtaining a copy of this software and associated documentation
//	files (the "Software"), to deal in the Software without
//	restriction, including without limitation the rights to use,
//	copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the
//	Software is furnished to do so, subject to the following
//	conditions:
//
//	The above copyright notice and this permission notice shall be
//	included in all copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//	EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//	OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//	HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//	WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//	FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//	OTHER DEALINGS IN THE SOFTWARE.

// Many thanks to Rainer Brockerhoff <http://brockerhoff.net> for some excellent suggestions on how to improve the Event Tap code


#import "Tabster.h"
#import <ApplicationServices/ApplicationServices.h>




//we can only have one keypress event tap without causing a bit of bother, so store a global reference to it
//that way we can check to see if it's been initialized
static CFMachPortRef eventTap=NULL;

@implementation Tabster

- (id)initWithPlugInController:(CodaPlugInsController*)inController bundle:(NSBundle*)aBundle
{
	if ( (self = [super init]) != nil )
	{
		//get a handle to the controller
		[self setController:inController];
			
		//start listening for key events
		[self listenForKeyEvents];
		
	}
	return self;
}

-(void) dealloc
{
	[controller release];
	[super dealloc];
}

- (NSString*)name
{
	return @"Tabster";
}


//set up an event tap listening for keyDown events
- (void)listenForKeyEvents
{
	CFRunLoopSourceRef runLoopSource;

	//we only want keydown events
	CGEventMask eventMask = (1 << kCGEventKeyDown);

	//if the event tap has already been initialized, remove it
	//this will only happen if the user has installed the bundle by double-clicking it
	//in this circumstance, Coda loads the plugin twice because it loads it,
	//then copies it to the Application Support folder and re-loads it
	if(eventTap)
	{
		CGEventTapEnable(eventTap, false);
		CFRelease(eventTap);
	}
		
	// Check to see if we are allowed to create a keyboard event tap
	if (!AXAPIEnabled()&&!AXIsProcessTrusted()) { 
		// error dialog here 
		NSAlert *alert = [[[NSAlert alloc] init] autorelease];
		[alert addButtonWithTitle:@"OK"];
		[alert setMessageText:[NSString stringWithFormat:@"Could not enable the %@ plugin.",[self name]]];
		[alert setInformativeText:@"Please enable \"access for assistive devices\" in the Universal Access pane of System Preferences."];
		[alert setAlertStyle:NSCriticalAlertStyle];
		[alert runModal];
		return;
	} 
	
	//get Coda's process ID
	ProcessSerialNumber psn;
	if(GetCurrentProcess(&psn) != noErr)
	{
		//bail out if we can't get the PID
		NSDictionary* errorDict=[NSDictionary dictionaryWithObjectsAndKeys:
								 [NSString stringWithFormat:@"Initialization of %@ plug-in failed.",[self name]], NSLocalizedDescriptionKey,
								 @"Could not obtain the process ID of the Coda application.", NSLocalizedRecoverySuggestionErrorKey,
								 nil];
		NSError* error=[[NSError alloc] initWithDomain:@"TabsterErrorDomain" code:-1 userInfo:errorDict];
		[NSApp presentError:error];
		return;
	}
	
	//create the event tap just for Coda
	eventTap = CGEventTapCreateForPSN(&psn,kCGHeadInsertEventTap, 0, eventMask, myKeyDownCGEventCallback, self);	
	
	// Create a run loop source.
	runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
	
	// Add to the current run loop.
	CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
	
	// Enable the event tap.
	CGEventTapEnable(eventTap, true);
}

#pragma mark -
#pragma mark accessors
- (CodaPlugInsController *)controller
{
    return [[controller retain] autorelease]; 
}

- (void)setController:(CodaPlugInsController *)aController
{
    [controller release];
    controller = [aController retain];
}


@end

//the CGEvent callback that does the heavy lifting
CGEventRef myKeyDownCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
{
	//make sure this really is a key down event
	if ((type != kCGEventKeyDown))
		return event;
	
	//get a handle on the tabster object
	Tabster* tabster=(Tabster*) refcon;
	if(!tabster)
		return event;
	
	
	//don't process anything if there are modifiers down (except shift)
	CGEventFlags cgModifiers = CGEventGetFlags(event);
	if(cgModifiers & (kCGEventFlagMaskCommand | kCGEventFlagMaskControl | kCGEventFlagMaskAlternate))
		return event;
	
	//get the keypress and look up our replacements array to see if we have a match
	NSString *keyString = nil;
	UniCharCount charLen;
	
	CGEventKeyboardGetUnicodeString (event, 0, &charLen, NULL);
	if (charLen)
	{
		UniChar *chars = malloc(charLen * sizeof(UniChar));
		CGEventKeyboardGetUnicodeString (event, charLen, &charLen, chars);
		@try {
			keyString = [NSString stringWithCharacters:chars length:charLen];
		}
		@catch (NSException *exception)
		{
			NSLog (@"Event tap key event stringWithCharacters exception: %@, %@", [exception name], [exception reason]);
		}
		@finally {
			free (chars);
		}
	}
	if([keyString length]==0)
	{
		return event;
	}
	
	BOOL useTab=([keyString characterAtIndex:0] == NSTabCharacter);
	BOOL useShiftTab=(useTab && ((cgModifiers & kCGEventFlagMaskShift) == kCGEventFlagMaskShift));
	if(useTab || useShiftTab)
	{
		//get the coda text view object
		CodaTextView* tv = [[tabster controller] focusedTextView:nil];
		
		//if there isn't one, just bail out and return the unaltered event
		if(!tv)
			return event;
		
		
		//check to see that we're not entering text anywhere but the main text view
		id firstResponder=[[tv window] firstResponder];
		if([firstResponder respondsToSelector:@selector(isFieldEditor)])
		{
			if([firstResponder isFieldEditor])
				return event;
		}
		
		//check to see that we have a multi-line selection
		NSString* selectedText=[tv selectedText];
		if(!selectedText)
			return event;
		if([selectedText rangeOfString:[tv lineEnding]].location == NSNotFound)
		{
			return event;
		}
		
		//we are using the shiftLeft: and shiftRight: selectors in Coda depending on whether or not the shift key is down
		SEL shiftSelector = useShiftTab ? @selector(shiftLeft:) : @selector(shiftRight:);
		
		//get a responder that can handle the shift selector by walking the responder chain
		@try {
			while(firstResponder && ![firstResponder respondsToSelector:shiftSelector])
			{
				firstResponder = [firstResponder nextResponder];
			}
		}
		@catch (NSException *exception)
		{
			NSLog (@"Find responder for %@ selector exception: %@, %@",NSStringFromSelector(shiftSelector), [exception name], [exception reason]);
		}
		@finally {
		}

		//perform the shift in the next event loop
		if(firstResponder)
		{
			[firstResponder performSelector:shiftSelector withObject:nil afterDelay:0];
			return NULL;
		}
		return event;
	}
	
	return event;
}
